fix(sysio): preserve epoch batch reward rosters (WIRE-343) - #576
Conversation
Change-Id: Iaa60f6d7cfad4dc0662b5b81b6e8781acf120741
heifner
left a comment
There was a problem hiding this comment.
Design looks sound — the roster recorded by rcrdbatch is read after the window slide so it matches the epoch/group accrueepoch credits, the KV key is big-endian so batchepochs iterates in ascending epoch order (contiguity walk is valid), the erase(it) loop is safe, and ABI table_id 22503 has no collisions.
5 findings inline: 2 chain-halt risks on the mandatory advance inline path, 1 missing build artifact, 2 minor.
Change-Id: I0cb745aebe72c6b63c0bc7bbe0af94254b27dd52
heifner
left a comment
There was a problem hiding this comment.
Re-reviewed at 1ab98aff5 (after the artifact refresh). Status of the 2026-08-20 findings, plus one new one.
Resolved
sysio.system.abi:2124— artifact mismatch.1ab98aff5adds bothsysio.system.wasmandsysio.epoch.wasm, so the source-tree pair is consistent again. One nit for the squash: that commit touches only the two.wasmfiles with no accompanying source change, which is the shapecommit-wasm-only-with-source-change.mdbans per-commit. The PR as a whole pairs source with artifacts, so this is history hygiene rather than a provenance gap.
Still open, unchanged at head
emissions.cpp:684—rcrdbatch's cap throw on the mandatory inlineadvancepath.emissions.cpp:823— incomplete-history hard abort. One more concrete trigger added in that thread.emissions.cpp:1043— the clear loop is still outside theaccrued_epochs > 0guard: the guard opens at L1035 and closes at L1039, andbatch_history.eraseruns unconditionally at L1043.sysio.epoch.cpp:889—payepoch's now-unused second parameter is still serialized on every pay epoch.
New
emissions.cpp:678— an O(cadence × roster) deserialize on everyadvance, used only to produce a count. Inline below.
Change-Id: Ic6236e59b9d59eb8727d55228ceaea2d9b8aee86
heifner
left a comment
There was a problem hiding this comment.
Re-reviewed at 619f5d0a5. All six findings from the previous rounds are fixed — confirmed against the source, so I'm resolving those threads:
| Finding | Fix |
|---|---|
emissions.cpp:684 cap throw → chain halt |
Counting loop deleted; O(1) contains/erase probe of epoch_index - MAX_PAY_CADENCE_EPOCHS, guarded against the uint32_t underflow |
emissions.cpp:823 incomplete history → chain halt |
check removed; non-halting retain path |
period_start_epoch == 0 ? 1 seed |
Seeds from the earliest recorded roster |
emissions.cpp:1043 erase outside the guard |
Now inside if (accrued_epochs > 0) |
sysio.epoch.cpp:889 unused payepoch arg |
Now sends an empty vector |
| ABI/wasm mismatch | Fixed in 1ab98aff5 |
The effective_pay_cadence_epochs clamp is a good addition I hadn't asked for. Payout math, roster/epoch attribution, the contiguity triple, big-endian KV ordering, and erase(const_iterator) safety in the clear loop all verified sound.
Four new findings inline. The first one is a refinement of my own earlier suggestion — I proposed "print and retain in treasury" to remove the halt cliff, and retained turns out to mean permanently absorbed, which isn't what I intended.
Change-Id: I53a5b7164995c9157a9dbe0a0a6e769a5d26cc0a
heifner
left a comment
There was a problem hiding this comment.
Re-reviewed at dab8091ed. Converging — the previous round's four findings are addressed and only two new ones surfaced, one of which is a residual of finding 1 rather than something new.
Verified this round: the period length is provably <= MAX_PAY_CADENCE_EPOCHS, so rcrdbatch's exact epoch_index - 10 prune holds the table at <= 10 rows; sum(active_epochs) == accrued_epochs by construction, so the two integer divisions partition the pools exactly with no div-by-zero; batch_payout_credits is provably <= 100 for any setter-accepted config; the runtime credit cap is fail-safe (it routes to retention rather than paying some operators and not others); and the reworked epochlog prune is exact.
Two items below, plus a question about the cadence ceiling that I think needs a decision before this merges.
Change-Id: Ifd82421f9204fe64c782a87f1daad096d17c03b5
Change-Id: I0ee8c3f486a722a3dc72f8914986b2e5b11d0c7b
heifner
left a comment
There was a problem hiding this comment.
Re-reviewed at 5acf5d6c44. The four findings from the previous round are fixed. Since the change since then is the master merge, I checked the merge itself rather than just the source diff:
- Both conflicted wasms were rebuilt from merged source, not resolved by picking a side: each blob differs from both parents, the merged
sysio.epoch.wasmcarries master's SOL-378 withhold diagnostic alongside this PR's cadence check, and master's removeddelsnapprovis absent from the mergedsysio.system.wasm. - The merge tree differs from the mechanical auto-merge in exactly those two blobs — no source edits rode along — and every other contract artifact is byte-identical to master.
- The merged ABI is the exact union:
rcrdbatch/batch_epoch/batchepochs/ the threeepochlogfields added, master'sdelsnapprovandthreshold_pctdropped. - No semantic conflict with SOL-378:
rcrdbatchstill receives the group on duty now while the attestation ships the lookahead index, and the empty-group withhold skips the queueout without returning fromadvance— sorcrdbatchstill pairs with everyaccrueepochand this PR's contiguity invariant holds. - WIRE-375's symbol refactor is constant-only (same
{"WIRE", 9}) and leavesdrainrewardsuntouched, so the fee-deferral path is unaffected.
On the round-4 fixes: the credit-cap trip and the over-long-history break both set batch_history_complete = false before the drain gate, so an over-budget roster no longer sweeps a bucket it cannot distribute; effective_pay_cadence_epochs divides by max(operators_per_epoch, 1); and the reserv side tolerates deferral (add_capped_u64 accrual, get_reserv_rewards_balance clamped to asset::max_amount).
Two low items inline. Worth watching this head's CI specifically: the merge pairs master's regenerated unittests/snapshots/* and deep-mind.log with two freshly rebuilt production wasms, a combination neither side has run.
Change-Id: I36b05aa02a1db9fba6fa19df0be44cefbfc958ea
Summary
batchepochs, so delayed batch payouts use the historical recipients that actually earned each epoch's credits.pay_cadence_epochsto 1-10 and each payout to at most 100 recipient credits. With the intended 21-operator roster, the supported maximum cadence is 4 epochs (about 24 minutes at six-minute epochs); raising this conservative execution bound requires separate CPU/KV evidence.sysiotreasury and leave the fee bucket insysio.reservfor the next payable period. Complete but entirely empty roster history likewise preservessysio.reservcustody instead of stranding the bucket in the treasury.epochlogrecords history completeness and retained amounts.epochloghistory by completed payment rows, expose the completeness/retention audit fields through the generated ABI, and document runtime, deployment, custody, and monitoring invariants.sysio.systemcadence whensysio.epoch::setconfigchangesoperators_per_epoch, including a diagnostic that identifies the stored system setting.masterand regenerate the conflictingsysio.epochandsysio.systemWASM artifacts from the combined sources.Validation
fd9fbd2f9c350f30ba77af5bc7749638b01f4a5c.contracts_unit_testalso passed. Focused source and artifact system suites each passed 619 selected cases / 29,536 assertions, including the all-empty custody case and the incomplete-period-to-complete-period fee recovery sequence.sysio.epochandsysio.systemWASM files match the combined-source build outputs byte-for-byte.flow-emissions-soakruns passed all 81 phases / 311 steps, including the 1,800-second stability loop. Exact-head CI and flow evidence are being refreshed after the merge.Reviewer notes
epochlogrows is intentionally out of scope.sysio.systemandsysio.epochcontract updates together during a quiesced window before resuming epoch processing.operators_per_epoch * pay_cadence_epochs <= 100; update epoch capacity before system cadence when increasing the supported product.flow-emissions-soakare pending after review follow-upfd9fbd2f9c.